Skip to main content

第 4 章:組態檔語法與撰寫原則

原始類型

  • string 代表 Unicode 字串,例如:"hello"
  • number 代表數字:整數、小數
  • bool 代表布林值:truefalse
    • number 和 bool 都可以和 string 進行隱式轉換,當我們把 number 或 bool 類型的值賦給 string 類型的值,或是反過來時,Terraform 會自動替我們轉換類型,其中:
  • true"true"
  • false"false"
  • 15"15"
  • 3.1415"3.1415"

複雜類型

集合類型 list、map、set

一個集合包含了一組同一類型的值。集合內元素的類型成為元素類型。一個集合變數在構造時必須確定集合類型。集合內所有元素的類型必須相同。

  • list(...)

列表是一組值的連續集合,可以用下標存取內部元素,下標從 0 開始。例如名為 l 的 list,l[0] 就是第一個元素。list 類型的宣告可以是 list(number)、list(string)、list(bool) 等,括號中的類型即為元素類型。

  • map(...)

字典類型(映射類型),代表一組鍵唯一的鍵值對,鍵類型必須是 string,值類型任意。map(number) 代表鍵為 string 類型而值為 number 類型,其餘類推。 map 值有兩種宣告方式

  • { "foo": "bar", "bar": "baz" }

  • { foo="bar", bar="baz" }

  • 鍵可以不用雙引號,但如果鍵是以數字開頭則例外

  • 多對鍵值對之間要用逗號分隔,也可以用換行符號分隔。建議使用 = 號

  • set(...)

集合類型,代表一組不重複的值。

以上集合類型都支援通配類型縮寫,例如 list 等價於 list(any)map 等價於 map(any)set 等價於 set(any)any 代表支援任意的元素類型,前提是所有元素都是一個類型。例如,將 list(number) 賦給 list(any) 是合法的,list(string) 賦給 list(any) 也是合法的,但是 list 內部所有的元素必須是同一種類型的。

結構化類型 object、tuple

一個結構化類型允許多個不同類型的值組成一個類型。結構化類型需要提供一個 schema 結構資訊作為參數來指明元素的結構。

  • object(...)

    • 物件是指一組由具有名稱和類型的屬性所構成的符合類型
    • schema { \<KEY\>=\<TYPE\>, \<KEY\>=\<TYPE\>,...}
    • object({age=number, name=string})
    • 代表由名為 "age" 類型為 number ,以及名為 "name" 類型為 "string" 兩個屬性組成的物件。賦給 object` 類型的合法值必須含有所有屬性值,但是可以擁有多餘的屬性(多餘的屬性在賦值時會被拋棄)
    • 例如對 object({age=number,name=string})
      • { age=18 } 是一個非法值
      • { age=18, name="john", gender="male" } 是一個合法值,但賦值時 gender 會被拋棄
  • tuple(...)

    • 元組類似也是一組值的連續集合,但每個元素都有獨立的類型。
    • schema:[\<TYPE\>, \<TYPE\>, ...] 
    • 元組的元素數量必須與 schema 宣告的型別數量相等,且每個元素的型別必須與元組 schema 對應位置的型別相等。例如 tuple([string, number, bool]),類型的一個合法值可以是 ["a", 15, true] 複雜型別也支援隱式型別轉換。

Terraform 會嘗試轉換類似的類型,轉換規則有:

  • object 和 map:如果一個 map 的鍵集合含有 object 規定的所有屬性,那麼 map 可以被轉換為 objectmap 裡多餘的鍵值對會被拋棄。由 map → object → map 的轉換可能會遺失資料。
  • tuple 和 list:當一個 list 元素的數量剛好等於一個 tuple 宣告的長度時, list 可以轉換為 tuple。例如:值為 ["18", "true", "john"] 的 list 轉換為 tuple([number,bool, string]) 的結果為 [18, true, "john"]
  • set 和 tuple:當一個 list 或 tuple 被轉換為一個 set,那麼重複的值將被丟棄,並且值原有的順序也會遺失。如果一個 set 被轉換到 list 或是 tuple,那麼元素將按照以下順序排列:如果 set 的元素是 string,那麼將按照字段順序排列;其他類型的元素不承諾任何特定的排列順序。複雜型別轉換時,元素類型將在可能的情況下發生隱式轉換,類似上述 list 到 tuple 轉換舉的例子。
  • 如果類型不匹配,Terraform 會報錯,例如我們試圖把 object({name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12}) 轉換到 map(string) 類型,這是不合法的,因為 name 的值為 list,無法轉換為 string

佔位符 any

any 是 Terraform 中非常特殊的一種類型約束,它本身並非一個類型,而只是一個佔位符。每當一個值被賦予一個由 any 約束的複雜類型時,Terraform 會嘗試計算出一個最精確的類型來取代 any

例如我們把 ["a", "b", "c"] 賦給 list(any),它在 Terraform 中實際的物理類型首先被編譯成 tuple([string, string, string]) ,然後 Terraform 認為 tuple 和 list 相似,所以會嘗試將它轉換為 list(string)。然後 Terraform 發現 list(string) 符合 list(any) 的約束,所以會用 string 取代 any,於是賦值後最終的型別是 list(string)

由於即使是 list(any),所有元素的類型也必須是一樣的,所以某些類型轉換到 list(any) 時會對元素進行隱式類型轉換。例如將 ["a", 1, "b"] 賦給 list(any),Terraform 發現 1 可以轉換到 "1",所以最終的值是 ["a", "1", "b"],最終的型別會是 list(string)。再例如我們想把 ["a", \[\], "b"] 轉換成 list(any),由於 Terraform 無法找到一個合適的目標類型使得所有元素都能成功隱式轉換過去,所以 Terraform 會報錯,要求所有元素都必須是同一個類型的。

宣告類型時如果不想有任何的約束,那麼可以用 any

variable "no_type_constraint" {
type = any
}

無類型 null

代表資料缺失。如果我們把一個參數設為 null,Terraform 會認為你忘記為它賦值。如果該參數有預設值,那麼 Terraform 會使用預設值;如果沒有又剛好該參數是必填字短,Terraform 會報錯。null 在條件式中非常有用,你可以在某項條件不滿足時跳過對某參數的賦值。

object 的 optional 成員

如果一個 variable 的類型為 object,那麼使用時必須傳入一個結構完全相符的物件。

variable "an_object" {
type = object({
a = string
b = string
c = number
})
}

如果我們想要傳入一個物件給 var.an_object,但不準備給 b 和 c 賦值,我們必須這樣:

{
a = "a"
b = null
c = null
}

傳入的物件必須完全符合類型定義的結構,即使我們不想對某些屬性賦值。這使得我們如果想要定義一些比較複雜,屬性比較多的 object 類型時會給使用者在使用上造成一些麻煩。

with_optional_attribute
variable "with_optional_attribute" {
type = object({
a = string # a required attribute
b = optional(string) # an optional attribute
c = optional(number, 127) # an optional attribute with default value
})
}

這裡我們將 b 宣告為 optional,如果傳入的物件沒有 b,則會使用 null 作為值;c 不但宣告為 optional 的,還添加了 127 作為預設值,傳入的物件如果沒有 c,那麼會使用 127 作為它的值。

optional 修飾符有這樣兩個參數:

  • 類型:(必填)第一個參數標明了屬性的類型
  • 預設值:(選填)第二個參數定義了 Terraform 在物件中沒有定義該屬性值時所使用的預設值。預設值必須與類型參數相容。如果沒有指定預設值,Terraform 會使用 null 作為預設值。一個包含非 null 預設值的 optional 屬性在模組內使用時可以確保不會讀到 null 值。當使用者沒有設定該屬性,或是明確設定為 null 時,Terraform 會使用預設值,所以模組內無需再次判斷該屬性是否為 null

Terraform 採用自上而下的順序來設定物件的預設值,也就是說,Terraform 會先套用 optional 修飾符中的指定的預設值,然後再為其中可能存在的內嵌物件設定預設值。

帶有 optional 屬性和預設值的內嵌結構

下面的範例示範了一個輸入變數,用來描述一個儲存了靜態網站內容的儲存桶。此變數的型別包含了一系列的 optional 屬性,包括 website,不但其本身是 optional 的,其內部包含了數個 optional 的屬性以及預設值。

optional embedding
variable "buckets" {
type = list(object({
name = string
enabled = optional(bool, true)
website = optional(object({
index_document = optional(string, "index.html")
error_document = optional(string, "error.html")
routing_rules = optional(string)
}), {})
}))
}

以下給出一個範例 terraform.tfvars 文件,為 var.buckets 定義了三個儲存桶:

  • production 配置了一條重定向的路由規則
  • archived 使用了預設配置,但被關閉了
  • docs 使用文字檔案取代了索引頁和錯誤頁production 桶子沒有指定索引頁和錯誤頁,archived 桶子完全忽略了網站配置。Terraform 會使用 bucket 類型約束中指定的預設值。
terraform.tfvars
buckets = [
{
name = "production"
website = {
routing_rules = <<-EOT
[
{
"Condition" = { "KeyPrefixEquals": "img/" },
"Redirect" = { "ReplaceKeyPrefixWith": "images/" }
}
]
EOT
}
},
{
name = "archived"
enabled = false
},
{
name = "docs"
website = {
index_document = "index.txt"
error_document = "error.txt"
}
},
]

此配置會產生如下的 variable 值:

  • 對 production 和 docs 桶,Terraform 會將 enabled 設為 true。Terraform 會同時使用預設值配置 website,然後使用 docs 中指定的值來覆寫預設值。
  • 對 archived 和 docs 桶,Terraform 會將 routing_rules 設為 null。當 Terraform 沒有讀取到 optional 的屬性,且屬性上沒有設定預設值時,Terraform 會將這些屬性設為 null
  • 對於 archived 桶,Terraform 會將 website 屬性設為 buckets 類型約束中定義的預設值。
variable
tolist([
{
"enabled" = true
"name" = "production"
"website" = {
"error_document" = "error.html"
"index_document" = "index.html"
"routing_rules" = <<-EOT
[
{
"Condition" = { "KeyPrefixEquals": "img/" },
"Redirect" = { "ReplaceKeyPrefixWith": "images/" }
}
]

EOT
}
},
{
"enabled" = false
"name" = "archived"
"website" = {
"error_document" = "error.html"
"index_document" = "index.html"
"routing_rules" = tostring(null)
}
},
{
"enabled" = true
"name" = "docs"
"website" = {
"error_document" = "error.txt"
"index_document" = "index.txt"
"routing_rules" = tostring(null)
}
},
])

範例:有條件地設定一個預設屬性

有時我們需要根據其他資料的值來動態決定是否要為一個 optional 參數設定值。在這種場景下,發起呼叫的 module 區塊可以使用條件表達式搭配 null 來動態地決定是否設定該參數。

還是上一個例子中的 variable "buckets" 的例子,使用下面演示的例子可以根據新輸入參數 var.legacy_filenames 的值來有條件地覆蓋 website 對象中 index_document 以及 error_document 的設定:

define default value
variable "legacy_filenames" {
type = bool
default = false
nullable = false
}

module "buckets" {
source = "./modules/buckets"

buckets = [
{
name = "maybe_legacy"
website = {
error_document = var.legacy_filenames ? "ERROR.HTM" : null
index_document = var.legacy_filenames ? "INDEX.HTM" : null
}
},
]
}

當 var.legacy_filenames 設定為 true 時,呼叫會覆蓋 document 的檔名。當它的值為 false 時,呼叫不會指定這兩個檔名,這使得模組使用定義的預設值。

配置語法

參數

image_id = "abc123"

參數賦值就是將一個值賦給一個特定的名稱。參數值可以是確定的字面量硬編碼,也可以是一組表達式,用以透過其他的值加以計算結果值。

區塊

resource "aws_instance" "example" {
ami = "abc123"

network_interface {
# ...
}
}

一個區塊是包含一組其他內容的容器,一個區塊有一個類型(上面的例子裡類型就是 resource)。每個區塊類型都定義了類型關鍵字後面要跟多少標籤,例如 resource 區塊規定了後面要跟兩個標籤-在例子裡就是 aws_instance 和 example 。一個區塊類型可以規定任意多個標籤,也可以沒有標籤,例如內嵌的 network_interface 區塊。

在塊類型及其後續標籤之後,就是塊體。塊體必須被包含在一對花括號中間。在區塊體中可以進一步定義各種參數和其他的區塊。

Terraform 規範定義了有限多個頂級區塊類型,也就是可以遊離任何其他區塊獨立定義在設定檔中的區塊。大部分的 Terraform 功能(例如 resource, variable, output, data 等)都是頂級區塊。

標識符

參數名稱、區塊類型名稱以及其他 Terraform 規格中定義的結構的名稱,例如 resource、variable 等,都是識別碼。

合法的識別碼可以包含字母、數字、底線 (_) 以及減號 (-)。標識符首字母不可以為數字。

若要了解完整的識別碼規範,請造訪 Unicode 標識符語法

註釋

  • # 單行註釋,其後的內容為註釋
  • // 單行註釋,其後的內容為註釋
  • /* 和 /,多行註釋,可以註解多行預設情況下單行註解優先使用 #。自動化格式整理工具會自動把 // 換成 #。

編碼以及換行

Terraform 設定檔必須始終使用 UTF-8 編碼。分隔符號必須使用 ASCII 符號,其他識別碼、註解以及字串字面量均可使用非 ASCII 字元。

Terraform 相容於 Unix 風格的換行符以及 Windows 風格的換行符,但理想狀態下應使用 Unix 風格換行符。

輸入變數

如果我們想要在建立、修改基礎設施時動態傳入一些值呢?比如說在程式碼中定義 Provider 時用變數取代硬編碼的存取密鑰,或是由建立基礎架構的使用者來決定建立什麼樣尺寸的主機?我們需要的是輸入變數。

如果我們把一組 Terraform 程式碼想像成一個函數,那麼輸入變數就是函數的入參。輸入變數以 variable 區塊進行定義。

input variable
variable "image_id" {
type = string
}

variable "availability_zone_names" {
type = list(string)
default = ["us-west-1a"]
}

variable "docker_ports" {
type = list(object({
internal = number
external = number
protocol = string
}))
default = [
{
internal = 8300
external = 8300
protocol = "tcp"
}
]
}

這些都是合法的輸入參數定義。緊跟 variable 關鍵字的就是變數名。在一個 Terraform 模組(同一個資料夾中的所有 Terraform 程式碼文件,不包含子資料夾)中變數名稱必須是唯一的。我們在程式碼中可以透過 var.<NAME> 的方式引用變數的值。有一組關鍵字不可以被用來當作輸入變數的名字:

  • source
  • version
  • providers
  • count
  • for_each
  • lifecycle
  • depends_on
  • locals

輸入變數只能在宣告該變數的目錄下的程式碼中使用。

輸入變數區塊中可以定義一些屬性。

類型 type

可以在輸入變數區塊中透過 type 定義類型,例如:

variable "name" {
type = string
}
variable "ports" {
type = list(number)
}

定義了類型的輸入變數只能被賦予符合類型約束的值。

預設值 default

預設值定義了當 Terraform 無法獲得一個輸入變數得到值的時候會使用的預設值。例如:

variable "name" {
type = string
default = "John Doe"
}

當 Terraform 無法透過其他途徑獲得 name 的值時,var.name 的值為 "John Doe"

描述 description

可以在輸入變數中定義一個描述,簡單地向呼叫者描述該變數的意義和用法:

variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
}

如果在執行 terraform plan 或是 terraform apply 時 Terraform 不知道某個輸入變數的值,Terraform 會在命令列介面上提示我們為輸入變數設定一個值。例如上面的輸入變數程式碼,執行 terraform apply 時:

terraform apply
var.image_id
The id of the machine image (AMI) to use for the server.

Enter a value:

為了使的程式碼的使用者能夠準確地理解輸入變數的意義和用法,我們應該站在使用者而非程式碼維護者的角度編寫輸入變數的描述。描述並不是註解!

斷言 assert

確保輸入參數的類型是正確的

variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."

validation {
condition = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}

condition 參數是一個 bool 類型的參數,我們可以用一個表達式來定義如何界定輸入變數是合法的。

當 contidion 為 true 時輸入變數合法,反之不合法。condition 表達式中只能透過 var.\ 引用目前定義的變量,並且它的計算不能產生錯誤。

假如表達式的計算產生一個錯誤是輸入變數驗證的一種判定手段,那麼可以使用 can 函數來判定表達式的執行是否拋錯。例如:

variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."

validation {
# regex(...) fails if it cannot find a match
condition = can(regex("^ami-", var.image_id))
error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
}
}

在上述例子中,如果輸入的 image_id 不符合正規表示式的要求,那麼 regex 函數呼叫就會拋出一個錯誤,這個錯誤就會被 can 函數捕獲,輸出 false

condition 表達式如果為 false,Terraform 會傳 error_message 回中定義的錯誤訊息。error_message 應該完整描述輸入變數校驗失敗的原因,以及輸入變數的合法約束條件。

在命令列輸出中隱藏值

將變數設為 sensitive 可以防止我們在設定檔中使用變數時 Terraform 在 plan 和 apply 指令的輸出中展示與變數相關的值。

Terraform 仍然會將敏感資料記錄在狀態檔案中,任何可以存取狀態檔案的人都可以讀取到明文的敏感資料值。

宣告一個變數包含敏感資料值需要將 sensitive 參數設定為 true

variable "user_information" {
type = object({
name = string
address = string
})
sensitive = true
}

resource "some_resource" "a" {
name = var.user_information.name
address = var.user_information.address
}

任何使用了敏感變數的表達式都將被視為敏感的,因此在上面的範例中, resource “some_resource” “a” 的兩個參數也將在計劃輸出中被隱藏:

Terraform will perform the following actions:

# some_resource.a will be created
+ resource "some_resource" "a" {
+ name = (sensitive)
+ address = (sensitive)
}

Plan: 1 to add, 0 to change, 0 to destroy.

在某些情況下,我們會在巢狀區塊中使用敏感變量,Terraform 可能會將整個區塊視為敏感的。這發生在那些包含有要求值是唯一的內嵌區塊的資源中,公開這種內嵌區塊的部分內容可能會暗示兄弟區塊的內容。

 # some_resource.a will be updated in-place
~ resource "some_resource" "a" {
~ nested_block {
# At least one attribute in this block is (or was) sensitive,
# so its contents will not be displayed.
}
}

Provider 還可以將資源屬性宣告為敏感屬性,這將導致 Terraform 將其從常規輸出中隱藏。

如果打算使用敏感值作為輸出值的一部分,Terraform 會要求您將輸出值本身標記為敏感值,以確認確實打算將其匯出。

Terraform 可能暴露敏感變數的情況

sensitive 變數是一個以設定檔為中心的概念,值會毫無混淆地傳送給 Provider。如果該值包含在錯誤訊息中,則 Provider 報錯時可能會暴露該值。例如,即使「foo」 是敏感值,Provider 也可能傳回下列錯誤:"Invalid value 'foo' for field"

如果將資源屬性用作、或是作為 Provider 定義的資源 ID 的一部分,則 apply 將公開該值。在下面的範例中,前綴屬性已設定為 sensitive 變量,但隨後該值(“jae”)作為資源ID 的一部分公開:

sensitive
  # random_pet.animal will be created
+ resource "random_pet" "animal" {
+ id = (known after apply)
+ length = 2
+ prefix = (sensitive)
+ separator = "-"
}

Plan: 1 to add, 0 to change, 0 to destroy.

...

random_pet.animal: Creating...
random_pet.animal: Creation complete after 0s [id=jae-known-mongoose]

禁止輸入變數為空

輸入變數的 nullable 參數控制模組呼叫者是否可以將 null 指派給變數。

variable "example" {
type = string
nullable = false
}

nullable 的預設值為 true。當 nullable 為 true 時,null 是變數的有效值,且模組程式碼必須始終考慮變數值為 null 的可能性。將 null 作為模組輸入參數傳遞將覆蓋輸入變數上定義的預設值。

將 nullable 設為 false 可確保變數值在模組內永遠不會為空。如果 nullable 為 false 且輸入變數定義有預設值,則當模組輸入參數為 null 時,Terraform 將使用預設值。

nullable 參數僅控制變數的直接值可能為 null 的情況。對於集合或物件類型的變量,例如列表或對象,呼叫者仍然可以在集合元素或屬性中使用null,只要集合或物件本身不為 null

對輸入變數賦值

命令列參數

對輸入變數賦值有幾種途徑,一種是在呼叫 terraform plan 或是 terraform apply 指令時以參數的形式傳入:

terraform apply -var="image_id=ami-abc123"
terraform apply -var='image_id_list=["ami-abc123","ami-def456"]'
terraform plan -var='image_id_map={"us-east-1":"ami-abc123","us-east-2":"ami-def456"}'

可以在一條指令中使用多個 -var 參數。

參數文件

第二種方法是使用參數檔。參數檔的後綴名可以是 .tfvars 或是 .tfvars.json.tfvars 檔案使用 HCL 語法,.tfvars.json 使用 JSON 語法。

以 .tfvars 為例,參數檔中以 HCL 程式碼對需要賦值的參數進行賦值,例如:

image_id = "ami-abc123"
availability_zone_names = [
"us-east-1a",
"us-west-1c",
]

後綴名為 .tfvars.json 的檔案用一個 JSON 物件來對輸入變數賦值,例如:

{
"image_id": "ami-abc123",
"availability_zone_names": ["us-west-1a", "us-west-1c"]
}

呼叫 terraform 指令時,透過 -var-file 參數指定要使用的參數文件,例如:

terraform apply -var-file="testing.tfvars"
terraform apply -var-file="testing.tfvars.json"

有兩種情況,你無需指定參數檔:

  • 目前模組內有名為 terraform.tfvars 或是 terraform.tfvars.json 的文件
  • 目前模組內有一個或多個後綴名為 .auto.tfvars 或是 .auto.tfvars.json 的文件Terraform 會自動使用這兩種自動參數檔對輸入參數賦值。

環境變量

可以透過設定名為 TF_VAR_<NAME> 的環境變數為輸入變數賦值,例如:

export TF_VAR_image_id=ami-abc123
terraform plan
...

在環境變數名稱大小寫敏感的作業系統上,Terraform 要求環境變數中的 \ 與 Terraform 程式碼中定義的輸入變數名稱大小寫完全一致。

環境變數傳值非常適合在自動化管線中使用,尤其適合用來傳遞敏感數據,類似密碼、存取金鑰等。

互動介面傳值

在前面介紹斷言的例子中我們看到過,當我們從命令列介面執行 terraform 操作,Terraform 無法透過其他途徑取得一個輸入變數的值,而該變數也沒有定義預設值時,Terraform 會進行最後的嘗試,在互動介面上要求我們給出變數值。

輸入變數賦值優先權

當上述的賦值方式同時存在時,同一個變數可能會被賦值多次。Terraform 會使用新值覆蓋舊值。

Terraform 載入變數值的順序是:

  1. 環境變數
  2. terraform.tfvars 文件(如果存在的話)
  3. terraform.tfvars.json 文件(如果存在的話)
  4. 所有的 .auto.tfvars 或 .auto.tfvars.json 文件,以字母順序排序處理
  5. 透過 var 或是 var-file 命令列參數傳遞的輸入變量,按照命令列參數中定義的順序加載

假如以上方式皆未能成功對變數賦值,那麼 Terraform 會嘗試使用預設值;對於沒有定義預設值的變量,Terraform 會採用互動介面方式要求使用者輸入一個。對於某些 Terraform 指令,如果執行時帶有 -input=false 參數停用了互動介面傳值方式,那麼就會報錯。

複雜型別傳值

透過參數檔傳值時,可以直接使用 HCL 或 JSON 語法對複雜型別傳值,例如 list 或 map。

對於某些場景下必須使用 -var 命令列參數,或是環境變數傳值時,可以用單引號引用 HCL 語法的字面量來定義複雜類型,例如:

export TF_VAR_availability_zone_names='["us-west-1b","us-west-1d"]'

由於採用這種方法需要手動處理引號的轉義,所以這種方法比較容易出錯,複雜類型的傳值建議盡量通過參數檔。

輸出值

我們在介紹輸入變數時提到過,如果我們把一組 Terraform 程式碼想像成一個函數,那麼輸入變數就是函數的入參;函數可以有入參,也可以有回傳值,同樣的,Terraform 程式碼也可以有回傳值,這就是輸出值。

大部分語言的函數只支援無回傳值或是單回傳值,但是 Terraform 支援多回傳值。在目前模組 apply 一段 Terraform 程式碼,執行成功後命令列會輸出程式碼中定義的回傳值。另外我們也可以透過 terraform output 指令來輸出目前模組對應的狀態檔中的回傳值。

輸出值的聲明

輸出值的聲明使用輸出區塊,例如:

output "instance_ip_addr" {
value = aws_instance.server.private_ip
}

output 關鍵字後面緊接的就是輸出值的名稱。在目前模組內的所有輸出值的名字都必須是唯一的。output 區塊內的 value 參數即為輸出值,它可以像上面的範例裡那樣某個 resource 的輸出屬性,也可以是任意合法的表達式。

輸出值只有在執行 terraform apply 後才會被計算,光是使用 terraform plan 並不會計算輸出值。

Terraform 程式碼中無法引用本目錄下定義的輸出值。

output 區塊還有一些可選的屬性:

description

output "instance_ip_addr" {
value = aws_instance.server.private_ip
description = "The private IP address of the main server instance."
}

sensitive

一個輸出值可以標記 sensitive 為 true,表示該輸出值含有敏感資訊。被標記 sensitive 的輸出值只是在執行 terraform apply 指令成功後會印出""以取代真實的輸出值,執行 terraform output 時也會輸出 "",但仍可以透過執行 terraform output -json 看到實際的敏感值。

需要注意的是,標記為 sensitive 輸出值仍然會被記錄在狀態檔案中,任何有權限讀取狀態檔案的人仍然可以讀取到敏感資料。

depends_on

關於 depends_on 的內容將在 resource 章節中詳細介紹,所以這裡我們只是粗略地介紹一下。

Terraform 會解析程式碼所定義的各種 data、resource,以及他們之間的依賴關係,例如,建立虛擬機器時用的 image_id 參數是透過 data 查詢而來的,那麼虛擬機器實例就依賴這個鏡像的 data, Terraform 會先建立 data,得到查詢結果後,再建立虛擬機器 resource。一般來說,data、resource 之間的建立順序是由 Terraform 自動計算的,不需要程式碼的編寫者明確指定。但有時有些依賴關係無法透過分析程式碼得出,這時我們可以在程式碼中透過 depends_on 明確聲明依賴關係。

一般 output 很少會需要明確依賴某些資源,但有一些特殊場景,例如在當前程式碼中呼叫一個模組(可以理解成調用另一個目錄中的 Terraform 程式碼創建一些資源)時,呼叫者希望在模組資源全部創建完畢以後才繼續後續的創建工作,這時我們可以為模組設計一個 output,透過 depends_on 明確聲明依賴關係,以確保該 output 必須在所有模組資源成功創建以後才能被讀取,這樣我們就可以在模組尺度上控制資源的建立順序。

output "instance_ip_addr" {
value = aws_instance.server.private_ip
description = "The private IP address of the main server instance."

depends_on = [
# Security group rule must be created before this IP address could
# actually be used, otherwise the services will be unreachable.
aws_security_group_rule.local_access,
]
}

我們不鼓勵針對 output 定義 depends_on,只能作為最後的手段加以應用。如果不得不針對 output 定義 depends_on,請務必透過註解說明原因,方便後人進行維護。

precondition

output 區塊從 Terraform v1.2.0 開始也可以包含一個 precondition 區塊。

output 塊上的 precondition 對應於 variable 塊中的 validation 塊。validation 區塊檢查輸入變數值是否符合模組的要求,precondition 確保模組的輸出值符合某種要求。我們可以透過 precondition 來防止 Terraform 把一個不合法的處置值寫入狀態檔。我們可以在適當的場景下通過 precondition 來保護上一次 apply 留下的合法的輸出值。

Terraform 在計算輸出值的 value 表達式之前執行 precondition 檢查,這可以防止 value 表達式中的潛在錯誤被激發。

局部值

有時我們會需要用一個比較複雜的表達式計算某一個值,並且重複使用之,這時我們把這個複雜表達式賦予一個局部值,然後反覆引用該局部值。如果說輸入變數相當於函數的入參,輸出值就相當於函數的回傳值,那麼局部值就相當於函數內定義的局部變數。

局部值透過 locals 區塊定義,例如:

locals {
service_name = "forum"
owner = "Community Team"
}

一個 locals 區塊可以定義多個局部值,也可以定義任意多個 locals 區塊。賦給局部值的可以是更複雜的表達式,也可以是其他 data、resource 的輸出、輸入變量,甚至是其他的局部值:

locals {
# Ids for multiple sets of EC2 instances, merged together
instance_ids = concat(aws_instance.blue.*.id, aws_instance.green.*.id)
}

locals {
# Common tags to be assigned to all resources
common_tags = {
Service = local.service_name
Owner = local.owner
}
}

引用局部值的表達式是 local.<NAME>(注意,雖然局部值定義在 locals 區塊內,但引用是務必使用 local 而不是 locals),例如:

resource "aws_instance" "example" {
# ...

tags = local.common_tags
}

局部值只能在同一模組內的程式碼中引用。

局部值可以幫助我們避免重複複雜的表達式,提升程式碼的可讀性,但如果過度使用也有可能增加程式碼的複雜度,使得程式碼的維護者更難理解所使用的表達式和值。適度使用局部值,僅用於重複引用相同複雜表達式的場景,未來當我們需要修改該表達式時局部值將使得修改變得相當輕鬆。

重載文件

一般來說 Terraform 會載入模組內所有的 .tf 和 .tf.json 文件,並要求文件內定義了一組無重複的物件。如果兩個檔案嘗試定義同一個對象,那麼 Terraform 會報錯。

在某些少見場景中,能夠用單獨的檔案重載已有物件配置的特定部分將會十分有用。比如說,由工程師編寫的設定檔能夠在執行時被程式產生的 JSON 檔案部分重載。

為支援這些少見場景,Terrform 會對後綴名為 override.tf 和 override.tf.json 的程式碼檔案進行特殊處理。對於名為 override.tf 和 override.tf.json 的程式碼檔案也會進行相同的特殊處理。

Terraform 一開始載入程式碼檔案時會跳過這些重載文件,然後才會依照字典順序一個一個處理重載文件。對重載檔案中定義的所有頂級區塊(resource、data 等),Terraform 會嘗試尋找對應的已有物件並且將重載內容合併進已有物件。

重載檔案只應使用於特殊場景,過度使用會使得讀者在閱讀原始程式碼檔案時被迫還要閱讀所有的重載檔案才能理解物件配置,從而降低了程式碼的可讀性。使用重載文件時,請在原始文件被重載的部分中添加相應註釋,提醒未來的讀者哪些部分會被重載文件修改。

如果我們有一個名為 example.tf 的程式碼檔案:

resource "aws_instance" "web" {
instance_type = "t2.micro"
ami = "ami-408c7f28"
}

然後我們建立一個名為 override.tf 的檔案:

resource "aws_instance" "web" {
ami = "foo"
}

Terraform 隨後會合併兩者,實際的配置會是這樣的:

resource "aws_instance" "web" {
instance_type = "t2.micro"
ami = "foo"
}

合併行為

不同的區塊類型有著微不同的合併行為,某些特定區塊內的特殊構造會以特殊形式被合併。

一般來說:

  • 重載檔案內的頂級區塊會和普通檔案內同類型同名的頂級區塊合併
  • 重載檔案內的頂級區塊設定冊參數會覆寫普通檔案內對應區塊內的同名參數
  • 重載區塊內的內嵌區塊會取代普通檔案內對應區塊內的所有同類型內嵌區塊。所有重載區塊內沒有定義的內嵌區塊在普通檔案內保持不變
  • 內嵌塊的內容不會進行合併
  • 合併後的區塊仍然需要符合對應區塊類型的所有驗證規則

如果有多個重載檔案定義了同一個頂級區塊,那麼重載效果是疊加的,後載入的重載區塊會在先前載入的重載區塊生效的基礎上合併。重載操作首先依照檔案名稱的字典序其次是在重載檔案中的位置決定執行順序。

有一些針對特定頂級區塊類型的特殊合併行為規則,我們將重載檔案中定義的區塊稱為重載區塊,重載區塊在普通檔案中對應的區塊稱為來源區塊:

合併 resource 區塊以及合併 data 區塊

在 resource 區塊內,所有 lifecycle 區塊的內容會依照參數逐條合併。比如說,一個重載塊只定義了 create_before_destroy 參數而源塊定義了 ignore_changes,那麼 create_before_destroy 被合併的同時 igonore_changes 將會被保留。

如果重載的 resource 區塊包含了一個或多個 provisioner,那麼來源區塊內所有的 provisioner 會被忽略。

如果重載的 resource 區塊內包含了一個 connection 區塊,那麼它將完全覆蓋所有來源區塊內定義的 connection 區塊

不允許在重載區塊內定義 depends_on 參數,那將會引發一個錯誤。

合併 variable 區塊

variable 區塊內參數的合併遵循上述的標準流程,但對於 type 和 default 參數的處理會有一些特殊的考慮。

如果來源區塊定義了 default 值而重載區塊修改了變數的 type,Terraform 會嘗試將 default 值轉換成新類型,如果轉換失敗則會報錯。

同樣的,如果來源區塊定義了 type 參數而重載區塊修改了 default 值,那麼新的 default 值必須能夠轉換成原先的型別。

合併 output 區塊

不允許在重載區塊內定義 depends_on 參數,這會引發一個錯誤。

合併 locals 區塊

所有的 locals 區塊都定義了一個或多個命名值。針對 locals 的合併會是依照命名值的名字逐條執行的,不論命名值是在哪個 locals 區塊內被定義的。

合併 terraform 區塊

如果重載區塊定義了 required_providers 參數,那麼它的值會被逐條合併,這就允許重載區塊在不影響其他 Provider 的情況下調整單一 Provider 的版本約束。

重載區塊內的 requeired_version 和 required_providers 里的配置完全覆蓋來源區塊內的相應配置。如果來源區塊和重載區塊都定義了 required_version,那麼來源區塊的配置就會被完全忽略。

程式碼風格規範

Terraform 推薦以下程式碼規格:

  • 使用兩個空格縮排
  • 同一縮排層級的多個賦值語句以等號對齊:
ami           = "abc123"
instance_type = "t2.micro"
  • 當塊體內同時有參數賦值以及內嵌塊時,請先寫參數賦值,然後是內嵌塊。參數與內嵌塊之間空一行分隔
  • 同時包含參數賦值以及元參數賦值的區塊,請先編寫元參數賦值語句,接著是參數賦值語句,之間空一行分隔。元參數區塊請置於區塊體的最後,空一行分隔
terraform example
resource "aws_instance" "example" {
count = 2 # meta-argument first

ami = "abc123"
instance_type = "t2.micro"

network_interface {
# ...
}

lifecycle { # meta-argument block last
create_before_destroy = true
}
}
  • 頂層區塊之間應空一行分隔。內嵌塊之間也應該空一行分隔,除非是相同類型的內嵌塊(例如 resource 塊內部多個 provisioner 塊)
  • 同類型區塊之間盡量避免插入其他類型區塊,除非不同類型區塊共同組成了一個有語義的家族(比方說,aws_instnace 資源內的 root_block_deviceebs_block_deviceephemeral_block_device 內嵌區塊共同構成了描述 AWS 區塊儲存的區塊家族,所以他們可以被混合編寫)。

Checks

過去我們可以在 resource 區塊裡的 lifecycle 區塊中驗證基礎設施的狀態。check 區塊填補了在 terraform apply 後驗證基礎設施狀態這項功能中的一塊空白。

check 區塊允許我們定義在每次 plan 以及 apply 操作後執行的自訂的驗證。check 區塊定義的驗證邏輯是作為 plan 和 apply 操作的最後一步執行的。

語法

你可以定義一個包含本地名稱的 check 區塊,其中可以定義一個有限作用範圍的 data 區塊,以及至少一個的斷言

下面的範例示範了載入 Terraform 官網並驗證 HTTP 回傳狀態碼為 200

check
check "health_check" {
data "http" "terraform_io" {
url = "https://www.terraform.io"
}

assert {
condition = data.http.terraform_io.status_code == 200
error_message = "${data.http.terraform_io.url} returned an unhealthy status code"
}
}

有限作用範圍的資料來源

我們可以在 check 區塊使用任意 Provider 提供的任意資料來源作為一個有限作用範圍的資料來源。

一個 check 區塊可以配一個可選的內嵌(也叫有限作用範圍)資料來源。該 data 區塊和普通的 data 區塊行為類似,但你不能在定義它的 check 區塊以外引用它。另外,如果一個有限作用範圍的資料來源運行時觸發了任意錯誤,這些錯誤將被標記為警告,不會阻止 Terraform 繼續執行操作。

你可以使用有限作用範圍的資料來源在 resource 的 lifecycle 外驗證相關基礎設施片段的狀態。在上面的例子裡,如果 terraform_io 資料來源在載入時發生錯誤,那麼我們將會收到一個警告而不是中斷執行的錯誤。

元參數

有限作用域的資料來源支援 depends_on 和 provider 元參數,但不支援 count 或 for_each 元參數。

depends_on 元參數配合有限作用域資料來源可以提供非常強大的能力。

假設上述範例中的 Terraform 網站是我們即將用同一目錄下的 Terraform 程式碼部署的,在第一次創建 Plan 時因為網站還沒有被創建,所以驗證會失敗,Terraform 總是會在一開始顯示一條讓人分心的警告訊息。

我們可以為該內嵌資料來源新增 depends_on 來確保該資料來源依賴某項組成基礎架構的必要資源,例如負載平衡器。這樣對該資料來源的檢查結果將保持 known after apply 直到依賴項建立完成。此策略避免了在配置階段產生無意義的警告訊息,直到在 plan 和 apply 操作的合適階段執行檢查。

該策略的一個問題是如果有限作用域資料來源所依賴的資源發生了變化,那麼 check 區塊將返回 known after apply 直到 Terraform 完成了對被依賴資源的更新。在某些情況下,這種行為將會引發一些問題。

我們推薦只有在內嵌資料來源依賴某項資源,但又沒有明確的引用其資料時使用 depends_on 元參數。

assert 斷言

我們在 check 區塊中使用 assert 區塊定義自訂的斷言條件。每個 check 區塊必須聲明至少一個或更多的 assert 區塊。每個 assert 區塊都包含了一個 condition 屬性與一個 error_message 屬性。

與其他自訂檢查(variable 中的 validation 以及 lifecycle 中的 precondition 和 postcondition)不同,assert 的斷言不會影響 Terraform 執行操作。失敗的斷言將以警告訊息的形式輸出而不會中斷後續的操作。這與其他諸如 postcondition 這樣的自訂檢查形成了對比,因為它們的檢查失敗會立即終止後續的 plan 以及 apply 操作,傳回錯誤訊息。

assert 區塊中的斷言條件表達式可以引用同一 check 區塊裡的內嵌資料來源數據,以及同一模組中的任意輸入參數、資源、資料來源、模組的輸出值。

check 區塊的元參數

check 塊目前不支援元參數。Terraform 團隊目前正在收集有關此功能的回饋。

是使用 check 區塊還是其他自訂條件檢查

check 區塊提供了 Terraform 中最靈活的驗證功能。我們可以在其中引用輸出值、輸入參數、資源以及資料來源的值。我們的確可以使用 check 區塊取代所有其他的自訂條件檢查,但這並不意味著我們應該這麼做。

check 與其他檢查最大的差別在於 check 區塊不會中斷 Terraform 的執行。我們需要將這種非阻塞性的行為特徵計入考量來決定採取何種檢查。

輸出值與輸入參數

輸出值的 precondition 以及輸入變數的 validation 都可以對輸入輸出值進行斷言。

這些檢查是用來阻止 Terraform 在資料有問題時繼續執行的。

舉例來說,如果輸入參數的值是無效的那麼任由 Terraform 執行整個配置文件並沒有什麼意義,這種情況下,check 塊只會輸出有關無效輸入參數的警告,不會打斷 Terraform 的執行,而 validation 區塊則會警告輸入參數值非法,並終止 Terraform 執行 plan 或 apply 操作。

resource 區塊的 precondition 與 postcondition

check 與 precondition 和 postcondition 的差異更加微妙。

precondition 是自訂條件檢查中最特殊的,因為它們是在資源的變更被計算或應用之前執行的檢查。決定使用 precondition 還是 postcondition 的考量也適用於選擇要使用 precondition 還是 check 區塊。

我們可以在 postcondition 與 check 區塊之間互換來驗證資源和資料來源。例如,我們可以把上述範例中的 check 區塊改寫成 postcondition,以下的 postcondition 區塊將會驗證對 Terraform 網站的請求是否回傳了狀態碼 200

data "http" "terraform_io" {
url = "https://www.terraform.io"

lifecycle {
postcondition {
condition = self.status_code == 200
error_message = "${self.url} returned an unhealthy status code"
}
}
}

check 和 postcondition 區塊都在 plan 或 apply 操作中驗證了 Terraform 網站是否回傳 200 狀態碼,它們的差異是發生錯誤時的行為。

如果是 postcondition 失敗,那麼就無法繼續執行。Terraform 會阻止任意後續的 plan 或 apply 操作。

我們建議使用 check 區塊來驗證基礎設施的整體狀態,僅在希望確保單一資源狀態符合預期時才使用 postcondition

Reference

HCL 語法公約